package edu.kit.csl.cratyli.datatypes;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import edu.kit.csl.cratyli.ui.Logger;

/**
 * An instance of this class represents a pronunciation dictionary. It is
 * implemented as associative array with the written representation of a
 * word as key, and a {@link Word} instance as value.
 */
public class Dictionary {
	
	private Map<String, Word> map;
	
	/**
	 * Loads the dictionary in Janus format from the file system. The word
	 * weights are initialized with 0.
	 * 
	 * @param dictFileName Path to the Janus dictionary file
	 * @return the loaded dictionary instance
	 * @throws IOException if dictFileName does not exist/is not readable
	 */
	static public Dictionary loadFromFile(String dictFileName) 
			throws IOException {
		Logger log = Logger.getSingleton();
		FileInputStream fstream = new FileInputStream(dictFileName);
		DataInputStream in = new DataInputStream(fstream);
		BufferedReader br = new BufferedReader(new InputStreamReader(in));
		Dictionary dict = new Dictionary();
		String line;
		while ((line = br.readLine()) != null) {
			String[] entry = line.trim().split("\\s+", 2);
			if (entry.length != 2) {
				log.warn(dictFileName + ": Invalid line '" + line + "'");
				continue;
			}
			dict.put(entry[0], new Word(entry[0],
				entry[1].replaceAll("\\{", " ").replaceAll("\\}", " ")
					.replaceAll(" WB", " ").trim()
					.replaceAll("\\s+", " ")));
		}
		in.close();
		return dict;
	}
	
	/**
	 * Write the dictionary to the file system in Janus format. The entries are
	 * ordered by their weight (decreasing)
	 * 
	 * @param fileName prefix for the output files
	 */
	public void writeToFileSystem(String fileName) throws IOException {
		List<Word> words = new ArrayList<Word>(map.values());
		Collections.sort(words);
		FileWriter fs = new FileWriter(fileName);
		BufferedWriter out = new BufferedWriter(fs);
		for (Word word : words) {
			out.write(word.getOrthographic() + " {" + 
				generateJanusPronunciation(word.getPronunciation()) + "}\n");
		}
		out.close();
	}
	
	/*
	 * Helper function for writeToFileSystem(). Generates a string in Janus
	 * format from a pronunciation.
	 * 
	 * @param pronun has to contain at least 1 element
	 */
	public String generateJanusPronunciation(String[] pronun) {
		String[] p = pronun.clone();
		p[0] = "{" + p[0] + " WB}";
		final int pronLen = p.length; 
		if (pronLen > 1) {
			p[pronLen - 1] = "{" + p[pronLen - 1] + " WB}";
		}
		StringBuilder b = new StringBuilder();
		for (String phone : p) {
			b.append(" " + phone);
		}
		return b.toString().trim();
	}
	
	/**
	 * Creates an empty dictionary.
	 */
	public Dictionary() {
		map = new HashMap<String, Word>();
	}
	
	/**
	 * Combines this dictionary with the given dictionary. Pronunciations of
	 * this dictionary will be overridden in case of duplicate keys.
	 * 
	 * @param dict the dictionary which entries should be added
	 * @throws NullPointerException if {@code dict} is null
	 */
	public void putAll(Dictionary dict) {
		map.putAll(dict.map);
	}
	
	/**
	 * Add a new entry to the dictionary. If the key exists, replace the old
	 * entry.
	 * 
	 * @param ortho written representation of the word
	 * @param word {@link Word} instance representing the pronunciation
	 */
	public void put(String ortho, Word word) {
		map.put(ortho, word);
	}
	
	/**
	 * Incorporates entries of the given dictionary in this dictionary. Weights
	 * of homophones are added and both spellings are added to this dictionary.
	 * In case of duplicate spelling, use this pronunciation and add weights.
	 * If the given dictionary contains unknown words, add it to the dictionary
	 * 
	 * @param dict dictionary to incorporate
	 */
	public void add(Dictionary dict) {
		HashMap<Word, Word> thisWords = new HashMap<Word, Word>();
		for (Word word : map.values()) {
			thisWords.put(word, word);
		}
		for (Entry<String, Word> entry : dict.map.entrySet()) {
			String ortho = entry.getKey();
			Word word = entry.getValue();
			if (map.containsKey(ortho)) {
				map.get(ortho).increaseWeight(word.getWeight());
			} else if (thisWords.containsKey(word)) { // Homophone
				Word thisWord = thisWords.get(word);
				thisWord.increaseWeight(word.getWeight());
				map.put(ortho, thisWord);
			} else {
				map.put(ortho, word);
			}
		}
	}
	
	/**
	 * Get the pronunciation by its written representation.
	 * 
	 * @param ortho the orthographic representation of the word
	 * @return corresponding {@link Word} instance (null if not existing) 
	 */
	public Word get(String ortho) {
		return map.get(ortho);
	}
	
	/**
	 * Get the number of entries in the dictionary.
	 */
	public int size() {
		return map.size();
	}
	
	/**
	 * Accumulates the word weights in the dictionary with their frequencies
	 * in the given corpus.
	 * 
	 * @param corpus the corpus over which counts should be collected
	 * @throws NullPointerException if {@code corpus} is null.
	 */
	public void collectWeights(TextCorpus corpus) {
		for (Entry<String, String> entry : corpus) {
			String[] words = entry.getValue().split(" ");
			for (String word : words) {
				if (map.containsKey(word)) {
					map.get(word).increaseWeight();
				}
			}
		}
	}
}
